Admission Webhook

개요

어드미션 컨트롤 도 웹훅을 설정하는 것이 가능하다.
어드미션 컨트롤에서의 유형과 마찬가지로, 여기에도 두 가지 유형이 존재한다.

먼저 변형이 완료된 이후에 최종적으로 검증이 들어가게 될 것이다.
순서가 이러니, 최종 상태를 확인하는 웹훅은 반드시 validating 쪽에 넣어야 할 것이다.
사용하기 위해서는 admissionregistration.k8s.io/v1 api가 활성화돼있어야 한다.

설정 방법

어드미션 웹훅 서버 만들기

https://github.com/kubernetes/kubernetes/blob/release-1.21/test/images/agnhost/webhook/main.go
일단 요청을 받으면 그에 맞는 응답을 하는 서버를 만들어야 한다.

요청 설정

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: "pod-policy.example.com"
webhooks:
- name: "pod-policy.example.com"
  rules:
  - apiGroups:   [""]
    apiVersions: ["v1"]
    operations:  ["CREATE"]
    resources:   ["pods"]
    scope:       "Namespaced"
  clientConfig:
    service:
      namespace: "example-namespace"
      name: "example-service"
    caBundle: <CA_BUNDLE>
  admissionReviewVersions: ["v1"]
  sideEffects: None
  timeoutSeconds: 5

만약 웹훅 서버에 인증이 필요하다면 api서버에 --admission-control-config-file에 파일을 넣어줘야 한다.

apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ValidatingAdmissionWebhook
  configuration:
    apiVersion: apiserver.config.k8s.io/v1
    kind: WebhookAdmissionConfiguration
    kubeConfigFile: "<path-to-kubeconfig-file>"
- name: MutatingAdmissionWebhook
  configuration:
    apiVersion: apiserver.config.k8s.io/v1
    kind: WebhookAdmissionConfiguration
    kubeConfigFile: "<path-to-kubeconfig-file>"

이런 파일을 넣으면 된다.

웹훅 설정

그래서 구체적으로 어떤 동작을 할지 로직을 써야겠지.
이를 위해서는 MutatingWebhookConfiguration, ValidatinWebhookConfiguration을 써주면 된다.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
...
webhooks:
- name: my-webhook.example.com
  rules:
  - operations: ["CREATE", "UPDATE"]
    apiGroups: ["apps"]
    apiVersions: ["v1", "v1beta1"]
    resources: ["deployments", "replicasets"]
    scope: "Namespaced"
  ...

이런 식으로 쓰면 되는데, 와일드카드를 사용하는 것도 가능하다.
라벨 셀렉터을 통해 요청을 매칭하는 것도 가능하다.

- name: my-webhook.example.com
  objectSelector:
    matchLabels:
      foo: bar
  namespaceSelector:
    matchExpressions:
  	- key: runlevel
  	  operator: NotIn
  	  values: ["0","1"]

  rules:
  - operations: ["CREATE"]
    apiGroups: ["*"]
    apiVersions: ["*"]
    resources: ["*"]
    scope: "*"

이런 식으로 해주면, 라벨에 걸린 오브젝트들에 어떤 규칙들을 지정하는 것이 될 것이다.
네임스페이스에 대해서도 성립한다.

matchPolicy

한꺼번에 여러 api 그룹과 버전을 지정하고 싶다면?
matchPolicy를 이용하면된다.
여기에는 두 가지 값이 가능하다.

대체로 후자가 추천되는데, 이후에 업데이트가 일어나 버전이 바뀌어도 웹훅이 적용될 것이기 때문이다.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
webhooks:
- name: my-webhook.example.com
  matchPolicy: Equivalent
  rules:
  - operations: ["CREATE","UPDATE","DELETE"]
    apiGroups: ["apps"]
    apiVersions: ["v1"]
    resources: ["deployments"]
    scope: "Namespaced"

이런 식으로 하면 그럴 일이야 없겠지만.. 이후에 디플로이먼트의 api 그룹이 변경되더라도 웹훅이 적용될 것이다.

matchConditions

    matchConditions:
      - name: 'exclude-leases' # Each match condition must have a unique name
        expression: '!(request.resource.group == "coordination.k8s.io" && request.resource.resource == "leases")' # Match non-lease resources.
      - name: 'exclude-kubelet-requests'
        expression: '!("system:nodes" in request.userInfo.groups)' # Match requests made by non-node users.
      - name: 'rbac' # Skip RBAC requests, which are handled by the second webhook.
        expression: 'request.resource.group != "rbac.authorization.k8s.io"'

이런 식으로 CEL 표현식을 사용하는 것도 가능하다.
이렇게 하면 HTTP 상에서의 상세한 필터링도 가능해질 것이다.
이 모든 조건을 충족해야만 웹훅이 날아간다.

여기에 특별한 변수들이 있다.

clientConfig

  clientConfig:
    url: "https://my-webhook.example.com:9443/my-webhook-path"

여기에 보낼 웹훅 주소를 넣는다.

  clientConfig:
    caBundle: <CA_BUNDLE>
    service:
      namespace: my-service-namespace
      name: my-service-name
      path: /my-path
      port: 1234

클러스터 내부 서비스로 있는 거라면 이렇게 지정해도 된다.

sideEffects

webhooks:
  - name: my-webhook.example.com
    sideEffects: NoneOnDryRun

하나의 오브젝트에 대해서 변형을 진행하나 이게 다른 곳에서 영향을 미칠 수도 있다.
이에 대한 회복 로직을 잘 마련해줘야 한다.
웹훅은 한번 요청을 변경은 해도 해당 요청이 잘 수행될지에 대한 검토를 하지 않는다.
그래서 dry run으로 오는 요청에 대해 제대로 응답을 해야 한다.
dry run은 그냥 테스트만 하는 요청이기 때문에 실제 변형을 짆애하지 않게 해야 한다.

reinvocationPolicy

- name: my-webhook.example.com
  reinvocationPolicy: IfNeeded

여러 mutating webhook이 동작하는 환경에서는 웹훅이 여러번 보내져야 하고, 이것들이 전부 적용되기 위해서는 이전 요청에 대한 응답이 제대로 다음 요청으로 보내져야 할 것이다.
이럴 때는 재호출 정책을 지정한다.
Never를 쓰면 한번만 호출이 일어나고, IfNeeded면 변형이 진행된 오브젝트가 다른 룰에 들어가야 할 때 재호출을 해준다.

주의점이 있는데, a와 b가 있는데 a로 인한 변형 때문에 b로 재호출이 일어나고, 다시 a로 호출이 가야한다면 이는 제대로 동작하지 않을 것이다.
또한 재호출을 최소화하기 위해 웹훅이 알아서 재정렬될 수도 있다.
전부제대로 됐는지 확인하려면 validatain webhhok 을 쓰자.

이래서 왠만해서 Idempotent해야 한다.

감사

api 서버에서는 웹훅으로 인해 어떤 상황이 일어났는지 이벤트로 남겨준다.

모범 사례

각 웹훅은 멱등하게 만드는 것이 좋다.
같은 요청이 몇 번을 들어와도 같은 응답을 내뱉게 만들어야 한다.
가령 파드 생성 요청에 현재 시간을 이름으로 가지는 사이드카 컨테이너를 넣는 것은 전혀 멱등하지 않다.
또 이미 사이드카가 있는데 같은 이름을 가진 사이드카를 또 넣는다던가 하는 식의 오류를 방지해야 한다.

관련 문서

이름 noteType created

참고